package ru.amse.stroganova.ui;

import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.JComponent;

import ru.amse.stroganova.presentation.GraphPresentation;
import ru.amse.stroganova.presentation.GraphStateListener;
import ru.amse.stroganova.ui.action.AlgorythmActionsFactory;
import ru.amse.stroganova.ui.action.ConvertionActionsFactory;
import ru.amse.stroganova.ui.action.EditingActionsFactory;
import ru.amse.stroganova.ui.command.Command;
import ru.amse.stroganova.ui.command.CommandFactory;
import ru.amse.stroganova.ui.tool.Tool;
import ru.amse.stroganova.ui.tool.ToolFactory;
import ru.amse.stroganova.ui.visual.GraphElementMarker;
import ru.amse.stroganova.ui.visual.GraphElementSelection;
import ru.amse.stroganova.ui.visual.VisualStateListener;

/**
 * A component for graph viewing and editing.
 * 
 * @author Dasha Stroganova
 * 
 */
public class GraphComponent extends JComponent {

	private static final int BUFFER_CAPACITY = 10;

	private GraphPresentation graphPresentation;
	
	private GraphElementSelection graphElementSelection;
	
	private GraphElementMarker graphElementMarker;

	private CommandFactory commandFactory;
	
	private final ToolFactory toolFactory;
	
	private final EditingActionsFactory editingActionsFactory;
	
	private final AlgorythmActionsFactory algorythmActionsFactory;
	
	private final ConvertionActionsFactory convertionActionsFactory;

	private Tool currentTool;
	
	private boolean wasChanged;

	private final UndoBuffer undoBuffer;
	
	private final MouseListener mouseListener = new MouseListener() {
 
		public void mouseClicked(MouseEvent e) {
			currentTool.mouseClicked(e);
		}

		public void mousePressed(MouseEvent e) {
			currentTool.mousePressed(e);
		}

		public void mouseReleased(MouseEvent e) {
			currentTool.mouseReleased(e);
		}

		public void mouseEntered(MouseEvent e) {
			currentTool.mouseEntered(e);
		}

		public void mouseExited(MouseEvent e) {
			currentTool.mouseExited(e);
		}
	};

	private final MouseMotionListener mouseMotionListener = new MouseMotionListener() {

		public void mouseDragged(MouseEvent e) {
			currentTool.mouseDragged(e);
		}

		public void mouseMoved(MouseEvent e) {
			currentTool.mouseMoved(e);
		}
	};

	private final VisualStateListener selectionStateListener = new VisualStateListener() {

		public void visualChangesCancelled() {
			editingActionsFactory.getDeleteAction().setEnabled(false);
			makeVisualChanges();
		}

		public void visualStateChanged() {
			editingActionsFactory.getDeleteAction().setEnabled(true);
			makeVisualChanges();
		}
		
	};
	
	private final VisualStateListener markerStateListener = new VisualStateListener() {

		public void visualChangesCancelled() {
			editingActionsFactory.getDiscardHighlightingAction().setEnabled(false);
			algorythmActionsFactory.messageUnactive();
			editingActionsFactory.getSelectAction().putValue(AbstractAction.SMALL_ICON, new ImageIcon(ClassLoader.getSystemResource("img/select.gif")));
			if (editingActionsFactory.isSelectActive()) {
				editingActionsFactory.getSelectAction().actionPerformed(null);
			}
			makeVisualChanges();
		}

		public void visualStateChanged() {
			editingActionsFactory.getDiscardHighlightingAction().setEnabled(true);
			makeVisualChanges();
		}
		
	};

	private final GraphStateListener graphStateListener = new GraphStateListener() {

		public void positionChanged() {
			makeVisualChanges();
		}

		public void structureChanged() {
			graphElementMarker.unmark();
			makeVisualChanges();
		}
	};
	
	private final PresentationController presentationController = new PresentationController() {
		public void setNewGraphPresentation(GraphPresentation presentation) {
			GraphComponent.this.removeCurrentPresentation();
			GraphComponent.this.attachNewPresentation(presentation);
			graphElementMarker.unmark();
			revalidateActions();
			makeVisualChanges();
		}
	};
	
	/**
	 * Creates component without graph to present.
	 *
	 */
	public GraphComponent() {
		toolFactory = new ToolFactory(this);
		editingActionsFactory = new EditingActionsFactory(this);
		algorythmActionsFactory = new AlgorythmActionsFactory(this);
		convertionActionsFactory = new ConvertionActionsFactory(this);
		currentTool = new Tool() {};//toolFactory.getNewSelectTool();
		undoBuffer = new UndoBuffer(BUFFER_CAPACITY);
		
		addMouseListener(mouseListener);
		addMouseMotionListener(mouseMotionListener);
	}
	
	/**
	 * Creates component with given presentation.
	 * 
	 * @param graphPresentation graph presentation
	 */
	public GraphComponent(GraphPresentation graphPresentation) {
		this();
		setGraphPresentation(graphPresentation);
	}
	
	/**
	 * Sets new graph presentation to this component
	 * 
	 * @param presentation new graph presentation
	 */
	public void setGraphPresentation(GraphPresentation presentation) {
		removeCurrentPresentation();
		undoClear();
		attachNewPresentation(presentation);
	}
	
	private void attachNewPresentation(GraphPresentation presentation) {
		if (this.graphPresentation == null) {
			enableEditingActions();
			graphElementSelection = new GraphElementSelection(presentation);
			graphElementSelection.addVisualStateListener(selectionStateListener);//visualStateListener);
			graphElementMarker = new GraphElementMarker(graphPresentation);
			graphElementMarker.addVisualStateListener(markerStateListener);//visualStateListener);
			
		}
		this.graphPresentation = presentation;
		graphPresentation.addGraphStateListener(graphStateListener);
		graphElementSelection.setGraphPresentation(graphPresentation);
		graphElementMarker.setGraphPresentation(graphPresentation);
		commandFactory = new CommandFactory(graphPresentation, graphElementSelection);
		currentTool = toolFactory.getSelectTool();
		editingActionsFactory.getSelectAction().actionPerformed(null);
		editingActionsFactory.getDeleteAction().setEnabled(false);
		revalidateActions();
		wasChanged = true;
		
		makeVisualChanges();
	}
	
	private void undoClear() {
		undoBuffer.clear();
		editingActionsFactory.getUndoAction().setEnabled(false);
		editingActionsFactory.getRedoAction().setEnabled(false);
	}
	
	private void removeCurrentPresentation() {
		if (graphPresentation == null) {
			return;
		}
		graphPresentation.removeGraphStateListener(graphStateListener);
	}
	
	private void enableEditingActions() {
		editingActionsFactory.getCircleLayoutAction().setEnabled(true);
		editingActionsFactory.getAddEdgeAction().setEnabled(true);
		editingActionsFactory.getAddVertexAction().setEnabled(true);
		editingActionsFactory.getSelectAction().setEnabled(true);
	}
	
	private void revalidateActions() {
		algorythmActionsFactory.getMinSpanningTreeAction().setEnabled(false);
		algorythmActionsFactory.getMaxFlowAction().setEnabled(false);
		if (graphPresentation.isDirected()) {
			algorythmActionsFactory.getTopSortAction().setEnabled(true);
			convertionActionsFactory.getToUndirectedAction().setEnabled(true);
			if (graphPresentation.isWeighted()) {
				algorythmActionsFactory.getMaxFlowAction().setEnabled(true);
			}
		} else {
			algorythmActionsFactory.getTopSortAction().setEnabled(false);
			convertionActionsFactory.getToUndirectedAction().setEnabled(false);
			if (graphPresentation.isWeighted()) {
				algorythmActionsFactory.getMinSpanningTreeAction().setEnabled(true);
			}
		}
		if (graphPresentation.isWeighted()) {
			algorythmActionsFactory.getDijkstraTreeAction().setEnabled(true);
			convertionActionsFactory.getToUnweightedAction().setEnabled(true);
			convertionActionsFactory.getToWeightedAction().setEnabled(false);
		} else {
			algorythmActionsFactory.getDijkstraTreeAction().setEnabled(false);
			convertionActionsFactory.getToUnweightedAction().setEnabled(false);
			convertionActionsFactory.getToWeightedAction().setEnabled(true);
		}
	}
	
	private void makeVisualChanges() {
		setPreferredSize(graphPresentation.getPreferredSize());
		revalidate();
		repaint();
	}

	/**
	 * Sets a tool to handle events.
	 * 
	 * @param tool
	 *            new tool
	 */
	public void setTool(Tool tool) {
		currentTool.finalActions();
		currentTool = tool;
	}

	/**
	 * Returns graph presentation.
	 * 
	 * @return graph presentation
	 */
	public GraphPresentation getGraphPresentation() {
		return graphPresentation;
	}

	/**
	 * Returns graph element selection.
	 * 
	 * @return graph element selection
	 */
	public GraphElementSelection getGraphElementSelection() {
		return graphElementSelection;
	}
	
	/**
	 * Returns graph element marker.
	 * 
	 * @return graph element marker
	 */
	public GraphElementMarker getGraphElementMarker() {
		return graphElementMarker;
	}
	
	/**
	 * Returns undo buffer.
	 * 
	 * @return undo buffer
	 */
	public UndoBuffer getUndoBuffer() {
		return undoBuffer;
	}
	
	/**
	 * Returns command factory.
	 * 
	 * @return command factory
	 */
	public CommandFactory getCommandFactory() {
		return commandFactory;
	}

	/**
	 * Returns tool factory.
	 * 
	 * @return tool factory
	 */
	public ToolFactory getToolFactory() {
		return toolFactory;
	}
	
	/**
	 * @return the presentationController
	 */
	public PresentationController getPresentationController() {
		return presentationController;
	}
	
	/**
	 * Returns component editing actions factory.
	 * 
	 * @return editing actions factory
	 */
	public EditingActionsFactory getEditingActionsFactory() {
		return editingActionsFactory;
	}
	
	/**
	 * Returns component algorythm actions factory.
	 * 
	 * @return algorythm actions factory
	 */
	public AlgorythmActionsFactory getAlgorythmActionsFactory() {
		return algorythmActionsFactory;
	}
	
	/**
	 * Returns component convertion actions factory.
	 * 
	 * @return convertion actions factory
	 */
	public ConvertionActionsFactory getConvertionActionsFactory() {
		return convertionActionsFactory;
	}
	
	/**
	 * @return the wasChanged
	 */
	public boolean wasChanged() {
		return wasChanged;
	}

	/**
	 * @param wasChanged the wasChanged to set
	 */
	public void setWasChanged(boolean wasChanged) {
		this.wasChanged = wasChanged;
	}
	
	/**
	 * Adds temrorary component to this component.
	 * 
	 * @param component
	 */
	public void addTempComponent(Component component) {
		add(component);
		revalidate();
		repaint();
	}

	/**
	 * Removes temprorary component from this component.
	 * 
	 * @param component
	 */
	public void removeTempComponent(Component component) {
		remove(component);
		revalidate();
		repaint();
	}

	/**
	 * Adds new command to this component.
	 * 
	 * @param command
	 */
	public void addCommand(Command command) {
		wasChanged = true;
		undoBuffer.pushCommand(command);
		editingActionsFactory.getUndoAction().setEnabled(true);
		editingActionsFactory.getRedoAction().setEnabled(false);
	}

	@Override
	protected void paintComponent(Graphics g) {		
		Graphics2D gr2D = (Graphics2D) g;
		gr2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		gr2D.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING,
				RenderingHints.VALUE_COLOR_RENDER_QUALITY);
		//TODO Graphics is casted to 2D and then all other methos see it as usual. is it good?
		if (graphPresentation != null) {
			graphPresentation.paint(g, getBackground());
			graphElementSelection.paint(g);
			graphElementMarker.paint(g, getBackground());
		}
		currentTool.paint(g, getBackground());
	}


}
